package datatypes;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;

import controller.DraftingHint;

import tools.Tool;
import Gui.TriangleDisplay;
import datatypes.list.LinkedList;
import datatypes.list.Node;
import datatypes.list.NodeStakeholder;

import static controller.Globals.*;

/**
 * A line segment in R^2 described by two points. 
 * @author PSeiferth
 *
 */
public class Edge implements Comparable<Edge>, NodeStakeholder<Edge> {
	public static long edgeCounter = 0;
	
	/**
	 * This Set of adjacentTriangles is maintained by
	 * {@link IndexedFaceSet}. Please do not modify
	 * its contents!
	 */
	private LinkedList<Triangle> trianglesLeft, trianglesRight;
	private Node<Edge> nodeToRecheck = null;
	
	private Vertex vertex1, vertex2;
	private DraftingHint status = DraftingHint.STANDARD;
	public long ID = edgeCounter++;
	
	public int userFlags = 0;
	
	public Edge(Vertex vertex1, Vertex vertex2) {
		this.setVertex1(vertex1);
		this.setVertex2(vertex2);
	}
	
	public Edge(Vertex vertex1, Vertex vertex2, DraftingHint status) {
		this.setVertex1(vertex1);
		this.setVertex2(vertex2);
		this.setStatus(status);
	}

	public void setVertex1(Vertex vertex) {
		this.vertex1 = vertex;
	}

	public Vertex getVertex1() {
		return vertex1;
	}

	public void setVertex2(Vertex vertex) {
		this.vertex2 = vertex;
	}

	public Vertex getVertex2() {
		return vertex2;
	}
	
//	public void setTrianglesLeft(LinkedList<Triangle> adjacentTriangles) {
//		this.trianglesLeft = adjacentTriangles;
//	}
//	
//	public void setTrianglesRight(LinkedList<Triangle> adjacentTriangles) {
//		this.trianglesRight = adjacentTriangles;
//	}

	public LinkedList<Triangle> getTrianglesLeft() {
		if (trianglesLeft == null) {
			trianglesLeft = new LinkedList<Triangle>();
		}
		return trianglesLeft;
	}
	
	public LinkedList<Triangle> getTrianglesRight() {
		if (trianglesRight == null) {
			trianglesRight = new LinkedList<Triangle>();
		}
		return trianglesRight;
	}
	
	public boolean addTriangleToAdjacentTriangles(Triangle triangle) throws Exception {
		if ( ! triangle.containsEdge(this)) {
			throw new Exception("Triangle does not contain this edge and is therefore not adjacent.");
		}
		
		Vertex offVertex = triangle.getVertexNotOnEdge(this);
		boolean added;
		if (Tool.leftOfTest(this, offVertex) > 0.00001 ) {
			added = getTrianglesLeft().add(triangle);
		} else {
			added = getTrianglesRight().add(triangle);
		}
		return added;
	}
	
	public void removeTriangleFromAdjacentTriangles(Triangle triangle) {
		getTrianglesLeft().remove(triangle);
		getTrianglesRight().remove(triangle);
	}
	
	public boolean isAdjacentToEdge(Edge other) {
		Vertex otherV1 = other.getVertex1();
		Vertex otherV2 = other.getVertex2();
			
		return 	otherV1.equals(vertex1) ||
				otherV1.equals(vertex2) ||
				otherV2.equals(vertex1) ||
				otherV2.equals(vertex2);
	}
	
	/**
	 * Returns a vertex of this Edge. The Vertex returned
	 * is not the same object as the knownVertex given as a 
	 * parameter. In other words, this method returns the
	 * other vertex if the knownVertex is one of the vertices
	 * of this edge. Otherwise the first vertex will be returned.
	 * 
	 * @param knownVertex
	 * @return
	 */
	public Vertex getOtherVertex(Vertex knownVertex) {
		if (this.getVertex1() == knownVertex) {
			return this.getVertex2();
		}
		return this.getVertex1();
	}
	
	public void setHighlighted(boolean highlighted) {
		if (highlighted) {
			this.setStatus(DraftingHint.HIGHLIGHTED);
		} else {
			this.setStatus(DraftingHint.STANDARD);
		}
	}
	
	public boolean getHighlighted() {
		return getStatus() == DraftingHint.HIGHLIGHTED;
	}
	
	/**
	 * Tests if this Edge is on the same locations as
	 * the passed in start and end point
	 * 
	 * It does so by comparing the two points x and y 
	 * coordinates to see if they are the same
	 * 
	 * @param start One connection vertex
	 * @param end The other connection vertex
	 * @return true if the edge joins those two points
	 */
	public boolean connectsCoordinates(Vertex start, Vertex end) {
		
		double xStart = start.getX();
		double yStart = start.getY();
		double xEnd = end.getX();
		double yEnd = end.getY();
		
		// Test if start is vertex1 and end is vertex2
		if (xStart == getVertex1().getX() &&
			yStart == getVertex1().getY() &&
			xEnd == getVertex2().getX() &&
			yEnd == getVertex2().getY())
			return true;
		
		// Test if start is vertex2 and end is vertex1
		if (xStart == getVertex2().getX() &&
			yStart == getVertex2().getY() &&
			xEnd == getVertex1().getX() &&
			yEnd == getVertex1().getY())
			return true;
		
		return false;
	}
	
	/**
	 * Tests if this Edge has the specified vertex as one
	 * of its sides
	 * 
	 * @param v The vertex to be tested
	 * @return true if the vertex is in there
	 */
	public boolean hasVertex(Vertex v) {
		double xStart = getVertex1().getX();
		double yStart = getVertex1().getY();
		double xEnd = getVertex2().getX();
		double yEnd = getVertex2().getY();
		
		if (v.getX() == xStart && v.getY() == yStart)
			return true;
		
		if (v.getX() == xEnd && v.getY() == yEnd)
			return true;
		
		return false;
	}
	
	
	/**
	 * Tests whether this edge intersects another edge.
	 * 
	 * This method uses the isLeftOf-test {@link tools.Tool_test#leftOfTest(Edge, Vertex)}.
	 * If the vertices of one edge are on the same side of another edge, those edges cannot intersect.
	 * 
	 * The epsilon parameter allows control of the precision of the algorithm. In a 
	 * perfect world, one would use this method with an epsilon value of 0. Unfortunately,
	 * we have to deal with the limitations of floating point precision. If a real-world-edge just
	 * touches another real-world-edge this algorithm might or might not report the edges to
	 * intersect, depending on rounding errors. An epsilon value greater than zero provides some
	 * allowance so as to report edges to be intersecting if they just barely don't. You may think
	 * of this behavior as if there was a snapping zone established around the edges.
	 * Adversely, a negative epsilon value results in the opposite: edges that intersect a
	 * tiny little bit will be reported as not intersecting, as if there was a small repelling
	 * zone established around each edge.
	 * 
	 * @param other The other edge.
	 * @param epsilon The desired level of floating point precision. Lower values ensue higher precision.
	 * @return true if the edges intersect.
	 */
	public boolean intersects(Edge other, double epsilon) {
		double pDetThisV1 = Tool.leftOfTest(other, this.vertex1);
		double pDetThisV2 = Tool.leftOfTest(other, this.vertex2);
		double pDetOtherV1 = Tool.leftOfTest(this, other.vertex1);
		double pDetOtherV2 = Tool.leftOfTest(this, other.vertex2);
		boolean result = false;
		
		// are both vertices of this edge on different sides of the other edge?
		if (pDetThisV1 <= epsilon && pDetThisV2 >= -epsilon || pDetThisV1 >= -epsilon && pDetThisV2 <= epsilon) {
			// are both vertices of the other edge on different sides of this edge?
			if (pDetOtherV1 <= epsilon && pDetOtherV2 >= -epsilon || pDetOtherV1 >= -epsilon && pDetOtherV2 <= epsilon) {
				result = true;
			}
		}
		
		return result;
	}
	
	/**
	 * Returns the angle between this edge and another edge
	 * Issues: http://www.euclideanspace.com/maths/algebra/vectors/angleBetween/issues/index.htm
	 * 
	 * @param otherEdge The other edge.
	 * @return the angle between the two lines
	 */
	public double angleWith(Edge otherEdge) {
        double angle1 = Math.atan2(getVertex1().getY() - getVertex2().getY(),
                                   getVertex1().getX() - getVertex2().getX());
        
        double angle2 = Math.atan2(otherEdge.getVertex1().getY() - otherEdge.getVertex2().getY(),
        						   otherEdge.getVertex1().getX() - otherEdge.getVertex2().getX());
        
        double resultRadians = angle1 - angle2;
        double resultDegrees = resultRadians * (180 / Math.PI);
        
        if (resultDegrees < 0)
        	resultDegrees = 360 + resultDegrees;
        
        return resultDegrees;
    }
	
	/**
	 * 
	 * Gives the weight of the edge. In this case it's just the length of the
	 * edge. NOTE: for comparing the length of two edges, us the more efficient
	 * {@link #getLengthSquared()} method instead.
	 * 
	 * @return the length of the edge
	 */
	public double getLength() {
		return Math.sqrt(getLengthSquared());
	}
	
	/**
	 * Returns the squared length of this edge.
	 * For comparing the length of two edges comparing the squared length
	 * suffices. This is faster than to compute the length so this is
	 * the preferred method for comparing lengths.
	 * @return The squared length of this edge.
	 */
	public double getLengthSquared() {
		return vertex1.distanceSquared(vertex2);
//		double deltaX = vertex1.getX() - vertex2.getX();
//		double deltaY = vertex1.getY() - vertex2.getY();
//		return deltaX * deltaX + deltaY * deltaY;
	}
	
	/**
	 * @return True if there is at least one each of left and right adjacent triangles.
	 */
	public boolean isDiagonalOfQuadrilateral() {
		return (trianglesLeft != null && trianglesLeft.size() > 0 &&
				trianglesRight != null && trianglesRight.size() > 0);
	}
	
	@Override
	public String toString() {
		return vertex1.toString() + "---" + vertex2.toString();
	}
	
	/**
	 * Returns a vertex representing a normal vector. Note, the 
	 * length is not necessarily 1.
	 * @return
	 */
	public Vertex getNormal() {
		double ex = vertex2.getX() - vertex1.getX();
		if (ex > -Double.MIN_VALUE && ex < Double.MIN_VALUE) {
			return new Vertex(1.0, 0.0);
		}
		double ey = vertex2.getY() - vertex1.getY();
		double x = -ey/ex;
		return new Vertex(x, 1.0);
	}
	
	/**
	 * Returns a vertex representing this edge's vector:
	 * (x2-x1, y2-y1)
	 * @return
	 */
	public Vertex getVector() {
		double ex = vertex2.getX() - vertex1.getX();
		double ey = vertex2.getY() - vertex1.getY();
		return new Vertex(ex, ey);
	}
	
	/**
	 * A vertex that is centered on this edge.
	 * @return
	 */
	public Vertex getCenter() {
		double ex = (vertex2.getX() - vertex1.getX())/2 + vertex1.getX();
		double ey = (vertex2.getY() - vertex1.getY())/2 + vertex1.getY();
		return new Vertex(ex, ey);
	}

	/**
	 * Returns the comparison of vertex1 if not zero; otherwise returns
	 * the comparison of vertex2.
	 * @param other
	 * @return
	 */
	@Override
	public int compareTo(Edge other) {
		int comparison = this.vertex1.compareTo(other.vertex1);
		if (comparison == 0) {
			comparison = this.vertex2.compareTo(other.vertex2);
		}
		return comparison;
	}

	@Override
	public void updateNode(Node<Edge> node) {
		
		setFirstEdgeToCheckForIntersection(node);
	}

	public void setFirstEdgeToCheckForIntersection(Node<Edge> intersectingEdge) {
		this.nodeToRecheck = intersectingEdge;
		if (this.nodeToRecheck != null) {
			this.nodeToRecheck.registerAsStakeholder(this);
		}
	}

	public Node<Edge> getFirstEdgeToCheckForIntersection() {
		return nodeToRecheck;
	}

	public void draw(Graphics graphics, TriangleDisplay c) {
		// nb.: do not make permanent changes to the passed-in Graphics object!
		Graphics2D g = (Graphics2D) graphics.create();
		g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
		boolean draw = true;
		switch (getStatus()) {
		case STANDARD: {
			g.setColor(EDGE_COLOR_STANDARD);
			break;
		}
		case HIGHLIGHTED: {
			g.setColor(Color.yellow);
			g.setStroke(new BasicStroke(2.0f));
			if (draw) {
				Vertex p0 = getVertex1();
				Vertex p1 = getVertex2();
				g.drawLine(p0.getX().intValue(), p0.getY().intValue(), p1.getX().intValue(), p1.getY().intValue());
				g.setColor(EDGE_COLOR_HIGHLIGHTED);
//				g.drawString("" + ID, (p1.getX().intValue() - p0.getX().intValue())/2 + p0.getX().intValue(), (p1.getY().intValue() - p0.getY().intValue())/2+p0.getY().intValue());
			}
			g.setColor(EDGE_COLOR_HIGHLIGHTED);
			float dash[] = { 5.0f };
			g.setStroke(new BasicStroke(0.5f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, dash, 0.0f));
			draw = DRAW_EDGE_LOCALLY_MINIMAL;
			break;
		}
		case BETA_SKELETON: {
			g.setColor(EDGE_COLOR_IN);
			g.setStroke(new BasicStroke(4.0f));
				Vertex p0 = getVertex1();
				Vertex p1 = getVertex2();
				g.drawLine(p0.getX().intValue(), p0.getY().intValue(), p1.getX().intValue(), p1.getY().intValue());
				
//				float dash[] = { 7.0f };
//				g.setStroke(new BasicStroke(2.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, dash, 0.0f));
//			g.setColor(Color.white);
//			draw = DRAW_EDGE_LOCALLY_MINIMAL;
			break;
		}
		case LOCALLY_MINIMAL: {
			g.setColor(EDGE_COLOR_LOCALLY_MINIMAL);
			float dash[] = { 5.0f };
			g.setStroke(new BasicStroke(0.5f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, dash, 0.0f));
			draw = DRAW_EDGE_LOCALLY_MINIMAL;
			break;
		}
		case CONVEX_HULL: {
			g.setColor(EDGE_COLOR_CONVEX_HULL);
			g.setStroke(new BasicStroke(2.0f));
			break;
		}
		case FAILED_DIAMOND_TEST: {
			g.setColor(EDGE_COLOR_FAILED_DIAMOND_TEST);
			break;
		}
		case IN: {
			g.setColor(EDGE_COLOR_IN);
			break;
		}
		case LMT_HOLE: {
			g.setColor(EDGE_COLOR_LMT_HOLE);
			draw = DRAW_EDGE_LMT_HOLE;
			break;
		}
		}
			
		if (draw) {
			Vertex p0 = getVertex1();
			Vertex p1 = getVertex2();
			g.drawLine(p0.getX().intValue(), p0.getY().intValue(), p1.getX().intValue(), p1.getY().intValue());
			if (getStatus() == DraftingHint.LOCALLY_MINIMAL) {
				g.setColor(Color.black);
				if (SHOW_EDGE_ID) {
					g.drawString("" + ID, (p1.getX().intValue() - p0.getX().intValue())/2 + p0.getX().intValue(), (p1.getY().intValue() - p0.getY().intValue())/2+p0.getY().intValue());
				}
			}
		}
	}

	public void setStatus(DraftingHint status) {
		this.status = status;
	}

	public DraftingHint getStatus() {
		return status;
	}
}
